#!/usr/bin/perl -w
# $Id: duckweed 2 2009-12-11 09:38:24Z duckweed.pl $
#
# duckweed -- a free replacement for the Amazon MP3 Downloader
#
# Given an .AMZ file downloaded from Amazon after purchasing an album from
# the MP3 store, duckweed will decrypt and download its contents.
#
# This program will not help you steal music, bypass DRM, or any similar act.
#
# To use duckweed, you will need the commandline curl utility, as well as
# the XML::Simple perl module.

use strict;

use Getopt::Long;
use IO::File;
use XML::Simple;
use Data::Dumper;

my $output_dir;
my $verbose;
my $debug;

sub usage {
    print "usage: $0 [ --output-dir DIR ] [ --verbose | --debug ] file.amz\n";
    exit 2;
}

GetOptions(
    'output-dir|d=s' => \$output_dir,
    'verbose|v' => \$verbose,
    'debug' => \$debug,
) || usage();

do_amz($_) for @ARGV;

exit 0;


sub do_amz {
    my $fn = shift;
    -r $fn or die "$fn does not exist or not readable";
    my $key = '29ab9d18b2449e315e72';
    my $iv = '5e72d79a11b34fee';
    my $cmd = "openssl des-cbc -base64 -d -in '$fn' -K $key -iv $iv";
    $debug && print STDERR "** decrypt with $cmd";
    my $decrypt = new IO::File "$cmd|" or die "openssl: $!";
    my $xml = join('',<$decrypt>);
    $xml or die "Decrypt failed (see openssl output)";
    $debug && print STDERR "** decrypted XML: $xml";

    my $album = parse_xml($xml);
    $debug && print STDERR "** parsed XML: ".Dumper($album);
    download_album($album);
}

sub sanitize_fn {
    my $string = shift || '';
    $string =~ tr/ /_/;
    $string =~ tr/A-Za-z0-9_\-()[]:.//cd;
    return $string;
}

sub download_track {
    my $album = shift;
    my $track = shift;
    my $out_fn = join('-',
        sanitize_fn($track->{artist}),
        sanitize_fn($track->{album} || $album->{title}),
        sanitize_fn(sprintf "%02d", int($track->{tracknum} || 0)),
        sanitize_fn($track->{title})) . '.mp3';
    my $out_path = $output_dir ? "$output_dir/$out_fn" : $out_fn;

    defined(my $pid = fork()) || die "fork: $!";
    if (! $pid) {
        open STDOUT, ">$out_path" or die "$output_dir/$out_fn: $!";
        close STDIN;
        exec 'curl', '--location', '--max-redirs', '5', $track->{url};
        die "exec: curl: $!";
    } else {
        $verbose and print STDERR
            "Downloading track $track->{tracknum}: $track->{title}\n";
        my $exit = waitpid $pid, 0;
        if ($? == 0) {
            $verbose and print STDERR "... saved to $out_path\n";
        } else {
            die "Download of $track->{url} failed";
        }
    }
}

sub download_album {
    my $album = shift;
    $verbose and print STDERR "Downloading $album->{artist}: $album->{title}\n";
    download_track($album, $_) for @{$album->{tracks}};
}

sub parse_xml {
    my $xml = shift;
    my $xs = new XML::Simple;
    my $doc = $xs->XMLin($xml, ForceArray => ['track']);
    $debug && print STDERR Dumper($doc);

    my $album = { title => $doc->{title}, artist => $doc->{creator} };
    for my $t (@{$doc->{trackList}->{track}}) {
        push @{$album->{tracks}}, {
            url => $t->{location},
            album => $t->{album},
            artist => $t->{creator},
            tracknum => $t->{trackNum},
            title => $t->{title},
        }
    }
    return $album;
}

# vi: set ts=4 sw=4 et:
